<!DOCTYPE html>
<html>
  <head>
    <title>
      Test Analyser getFloatFrequencyData and getByteFrequencyData, No
      Smoothing
    </title>
    <script src="../../resources/testharness.js"></script>
    <script src="../../resources/testharnessreport.js"></script>
    <script src="../resources/audit-util.js"></script>
    <script src="../resources/audit.js"></script>
    <script src="../resources/realtimeanalyser-testing.js"></script>
    <script src="../resources/fft.js"></script>
  </head>
  <body>
    <script id="layout-test-code">
      // Use a power of two to eliminate any round-off in the computation of the
      // times for context.suspend().
      let sampleRate = 32768;

      // The largest FFT size for the analyser node is 32768.  We want to render
      // longer than this so that we have at least one complete buffer of data
      // of 32768 samples.
      let renderFrames = 2 * 32768;
      let renderDuration = renderFrames / sampleRate;

      let audit = Audit.createTaskRunner();

      // Options for basic tests of the AnalyserNode frequency domain data.  The
      // thresholds are experimentally determined.  The threshold for the byte
      // frequency results could in general be off by 1 depending on very minor
      // differences in computing the FFT value and converting it to a byte
      // value (because Math.floor must be used).  For the tests that fail, set
      // |byteThreshold| to 1.  Using any threshold larger than this is a
      // serious error in the implementation of the AnalyserNode FFT.
      let testConfig = [
        {
          order: 5,
          // For this order, need to specify a higher minDecibels value for the
          // analyser because the FFT doesn't get that small. This allows us to
          // test that (a changed) minDecibels has an effect and that we
          // properly clip the byte data.
          minDecibels: -50,
          floatRelError: 9.6549e-7,
        },
        {order: 6, floatRelError: 1.0084e-5},
        {order: 7, floatRelError: 1.1473e-6},
        {order: 8, floatRelError: 1.0442e-6},
        {order: 9, floatRelError: 2.6427e-5},
        {order: 10, floatRelError: 2.9771e-5, byteThreshold: 1},
        {order: 11, floatRelError: 1.3456e-5},
        {order: 12, floatRelError: 4.6116e-7},
        {order: 13, floatRelError: 3.2106e-7},
        {order: 14, floatRelError: 1.1756e-7},
        {order: 15, floatRelError: 1.1756e-7}
      ];

      // True if all of the basic tests passed.
      let basicTestsPassed = true;

      // Generate tests for each entry in testConfig.
      for (let k = 0; k < testConfig.length; ++k) {
        let name = testConfig[k].order + '-order FFT';
        (function(config) {
          audit.define(name, (task, should) => {
            basicFFTTest(should, config).then(() => task.done());
          });
        })(testConfig[k]);
      }

      // Test that smoothing isn't done and we have the expected data, calling
      // getFloatFrequencyData twice at different times.
      audit.define('no smoothing', (task, should) => {
        // Use 128-point FFT for the test.  The actual order doesn't matter (but
        // the error threshold depends on the order).
        let options = {order: 7, smoothing: 0, floatRelError: 1.5684e-6};
        let graph = createGraph(options);
        let context = graph.context;
        let analyser = graph.analyser;

        // Be sure to suspend after the analyser fftSize so we get a full buffer
        // of data.  We will grab the FFT data to prime the pump for smoothing.
        // We don't need to check the results (because this is tested above in
        // the basicFFTTests).
        let suspendFrame = Math.max(128, analyser.fftSize);
        context.suspend(suspendFrame / sampleRate)
            .then(function() {
              // Grab the time and frequency data.  But we don't care what
              // values we get now; we just want to prime the analyser.
              let freqData = new Float32Array(analyser.frequencyBinCount);

              // Grab the frequency domain data
              analyser.getFloatFrequencyData(freqData);
            })
            .then(context.resume.bind(context));

        // Grab another set of data after one rendering quantum.  We will test
        // this to make sure smoothing was not done.
        suspendFrame += 128;
        context.suspend(suspendFrame / sampleRate)
            .then(function() {
              let timeData = new Float32Array(analyser.fftSize);
              let freqData = new Float32Array(analyser.frequencyBinCount);

              // Grab the time domain and frequency domain data
              analyser.getFloatTimeDomainData(timeData);
              analyser.getFloatFrequencyData(freqData);

              let expected =
                  computeFFTMagnitude(timeData, options.order).map(linearToDb);
              let comparison = compareFloatFreq(
                  Math.pow(2, options.order) + '-point float FFT', freqData,
                  expected, should, options);
              basicTestsPassed = basicTestsPassed && comparison.success;
            })
            .then(context.resume.bind(context));

        context.startRendering().then(() => task.done());
      });

      audit.run();

      // Run a simple test of the AnalyserNode's frequency domain data.  Both
      // the float and byte frequency data are tested.  The byte tests depend on
      // the float tests being correct.
      //
      // The parameters of the test are given by |options| which is a property
      // bag consisting of the following:
      //
      //  order:  Order of the FFT to test.
      //  smoothing:  smoothing time constant for the analyser.
      //  minDecibels:  min decibels value for the analyser.
      //  floatRelError:  max allowed relative error for the float FFT data
      function basicFFTTest(should, options) {
        let graph = createGraph(options);
        let context = graph.context;
        let analyser = graph.analyser;

        let suspendTime = Math.max(128, analyser.fftSize) / sampleRate;
        context.suspend(suspendTime)
            .then(function() {
              let timeData = new Float32Array(analyser.fftSize);
              let freqData = new Float32Array(analyser.frequencyBinCount);

              // Grab the time domain and frequency domain data
              analyser.getFloatTimeDomainData(timeData);
              analyser.getFloatFrequencyData(freqData);

              let expected =
                  computeFFTMagnitude(timeData, options.order).map(linearToDb);
              let comparison = compareFloatFreq(
                  Math.pow(2, options.order) + '-point float FFT', freqData,
                  expected, should, options);
              basicTestsPassed = basicTestsPassed && comparison.success;
              expected = comparison.expected;

              // For the byte test to be better, check that there are some
              // samples that are outside the range of minDecibels and
              // maxDecibels.  If there aren't the test should update the
              // minDecibels and maxDecibels values for the analyser.

              let minValue = Math.min(...expected);
              let maxValue = Math.max(...expected);

              should(minValue, 'Order: ' + options.order + ': Min FFT value')
                  .beLessThanOrEqualTo(analyser.minDecibels);
              should(maxValue, 'Order: ' + options.order + ': Max FFT value')
                  .beGreaterThanOrEqualTo(analyser.maxDecibels);
              // Test the byte frequency data.
              let byteFreqData = new Uint8Array(analyser.frequencyBinCount);
              analyser.getByteFrequencyData(byteFreqData);

              // Convert the expected float frequency data to byte data.
              let expectedByteData = convertFloatToByte(
                  expected, analyser.minDecibels, analyser.maxDecibels);

              should(byteFreqData, analyser.fftSize + '-point byte FFT')
                  .beCloseToArray(
                      expectedByteData,
                      {absoluteThreshold: options.byteThreshold || 0});
            })
            .then(context.resume.bind(context));

        return context.startRendering();
      }
    </script>
  </body>
</html>
